Skip to content

feat/phase-5-platform: hotkey, tray, close-to-tray, file-picker (stacked on #7)#8

Merged
StuartMeeks merged 3 commits into
masterfrom
feat/phase-5-platform
May 29, 2026
Merged

feat/phase-5-platform: hotkey, tray, close-to-tray, file-picker (stacked on #7)#8
StuartMeeks merged 3 commits into
masterfrom
feat/phase-5-platform

Conversation

@StuartMeeks

Copy link
Copy Markdown
Owner

Phase 5 of the build plan. The platform-services slice.

Base: `feat/phase-4-authoring` (PR #7). When the upstream stack merges, please retarget this PR to `master` and rebase.

What's live

  • Global hotkey via Win32 `RegisterHotKey`. Default Ctrl+Alt+S; pressing it anywhere brings Snipdeck's existing window to the foreground. `WindowsHotkeyService` subclasses the main window's WndProc with `SetWindowSubclass` to catch `WM_HOTKEY`.
  • Tray icon via `H.NotifyIcon`. Left-click → bring window forward. Right-click → context menu with Show Snipdeck and Exit.
  • Close-to-tray. When `AppConfig.CloseBehaviour = HideToTray` (the default), clicking the window's X cancels the close and hides the window instead. The process keeps running so the hotkey stays live. The tray's Exit flips an internal `_allowClose` flag and lets the next close pass through.
  • `IFilePickerService` abstraction (`WindowsFilePickerService` impl). `CliEditorDialog` no longer pokes Win32 directly — picker setup is centralised.
  • CLI cards render uploaded icons. `Identicon` user control gains an `IconRef` dependency property. When set, it resolves the absolute path via `IIconAssetStorage` and renders the uploaded PNG; falls back to the identicon otherwise.

New Core abstractions

  • `IHotkeyService` — `event Pressed`, `TryRegister(HotkeyBinding)`, `Unregister()`.
  • `ITrayService` — `event ShowRequested / ExitRequested`, `Initialise()`, `Dispose()`.
  • `IFilePickerService` — `PickImageAsync()` returning bytes + filename.

App-side platform implementations

  • `WindowsHotkeyService` — `LibraryImport`-based P/Invoke for `RegisterHotKey` / `UnregisterHotKey`, classic `DllImport` for `comctl32` subclassing. Key string maps to virtual-key codes for A–Z, 0–9, F1–F12, Space, Esc, Tab, Enter.
  • `HNotifyIconTrayService` — `H.NotifyIcon.WinUI.TaskbarIcon` with a Segoe MDL2 glyph as the icon, `ToolTipText = "Snipdeck"`, `MenuFlyout` for context.
  • `WindowsFilePickerService` — `FileOpenPicker` with PNG/JPG/BMP/WebP filter, HWND-bound via `WinRT.Interop.InitializeWithWindow`.

What CI doesn't verify (please test on Windows)

  • Ctrl+Alt+S from anywhere brings the window to the foreground (try when the window is hidden to the tray)
  • Tray icon appears with a Segoe glyph, left-click shows the window, right-click shows the menu, Exit actually exits
  • Clicking the window's X hides the window (default hide-to-tray) — Snipdeck stays in the tray
  • New CLI → choose an image → save → the CLI card on Home shows the uploaded image instead of the identicon

Not in this PR (deferred)

  • Live theme switching when settings change — still applies once at `MainWindow` ctor; Phase 6 wires the change-on-save path.
  • Settings UI for actually changing hotkey / close behaviour / theme — Phase 6.
  • CLI delete — still parked.

🤖 Generated with Claude Code

StuartMeeks and others added 3 commits May 29, 2026 17:40
WindowsHotkeyService registers the global hotkey via Win32 RegisterHotKey and
subclasses the main window's WndProc with SetWindowSubclass to catch
WM_HOTKEY. HotkeyModifiers map straight through to MOD_* constants
(intentional alignment from Phase 1). Default Ctrl+Alt+S registered on
startup; pressing it brings the window to the foreground from anywhere.

HNotifyIconTrayService creates an H.NotifyIcon TaskbarIcon with:
  - left-click → ShowRequested → bring window forward
  - right-click context menu: "Show Snipdeck" and "Exit"

Close-to-tray: App subscribes to AppWindow.Closing on the main window. When
config says HideToTray, Closing is cancelled and the window hides. The tray's
Exit flips an _allowClose flag so the next close passes through cleanly. The
process keeps running while hidden so the hotkey stays live.

IFilePickerService abstracts the FileOpenPicker setup (HWND init,
filters, byte readback). CliEditorDialog no longer pokes Win32 directly.

Identicon UserControl gains an IconRef dependency property; when set, it
resolves the absolute path via IIconAssetStorage and renders the uploaded
PNG instead of the identicon. CliCard binds both Seed (Cli.Id) and IconRef
so uploaded icons replace the identicon in the home grid.

This PR is the third in a stack of four. Base: feat/phase-4-authoring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alified App.Services

TaskbarIcon.IconSource is Microsoft.UI.Xaml.Media.ImageSource — FontIconSource
isn't assignable. Generate a stable identicon PNG at init time via
IdenticonService.GeneratePng and load it as a BitmapImage. The init has to
go async (image decode requires await), so ITrayService.Initialise becomes
ITrayService.InitialiseAsync.

Inside namespace Snipdeck.App.Controls, the unqualified 'App' identifier
doesn't resolve to Snipdeck.App.App (sibling namespaces aren't searched).
Fully qualify the call in Identicon.cs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vent handlers

- partial on HNotifyIconTrayService + its nested RelayCommand (CsWinRT1028).
- RelayCommand uses primary constructor (IDE0290).
- HNotifyIconTrayService menu construction switches to collection
  initialiser (IDE0017).
- App.xaml.cs hoists the lambda subscriptions to named methods so IDE0200
  stops flagging "(_, _) => Method()" patterns.
- WindowsHotkeyService converts SetWindowSubclass / RemoveWindowSubclass /
  DefSubclassProc from DllImport to LibraryImport with the source-generated
  marshalling (SYSLIB1054). SubclassProc parameters are marked
  UnmanagedType.FunctionPtr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Base automatically changed from feat/phase-4-authoring to master May 29, 2026 22:49
@StuartMeeks StuartMeeks merged commit 9852f3a into master May 29, 2026
2 checks passed
@StuartMeeks StuartMeeks deleted the feat/phase-5-platform branch May 29, 2026 22:49
StuartMeeks added a commit that referenced this pull request May 29, 2026
feat/phase-6-settings: editable settings + Velopack updater (stacked on #8)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant